using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace BOL.Maths
{
    /// <summary>
    /// Represents a Matrix generic class.
    /// </summary>
    public class Matrix<T> : ICloneable, IEnumerable<Vector<T>>, IEquatable<Matrix<T>>
    {
        #region Private variables

        private readonly int _m, _n;
        private readonly T[][] _elements;

        #endregion

        #region Public properties

        /// <summary>Returns the number of rows.</summary>
        public int M { get { return _m; } }

        /// <summary>Returns the number of columns.</summary>
        public int N { get { return _n; } }

        /// <summary>Returns the elements of the matrix.</summary>
        public T[][] Elements { get { return _elements; } }

        /// <summary>Returns the cell value given the row and column position.</summary>
        public T this[int i, int j]
        {
            get
            {
                if (0 > i || i >= _m || 0 > j || 0 >= _n)
                    throw new ArgumentOutOfRangeException();

                return _elements[i][j];
            }
            set
            {
                if (0 > i || i >= _m || 0 > j || 0 >= _n)
                    throw new ArgumentOutOfRangeException();

                _elements[i][j] = value;
            }
        }

        public bool IsSquare
        {
            get { return _elements.Length == _elements[0].Length; }
        }

        public bool IsSymmetric
        {
            get
            {
                if (IsSquare)
                {
                    for (var i = 0; i < _m; i++)
                        for (var j = 0; j <= i; j++)
                            if (!_elements[i][j].Equals(_elements[j][i]))
                                return false;

                    return true;
                }

                return false;
            }
        }

        /// <summary>Returns the row vectors.</summary>
        public IEnumerable<Vector<T>> Rows
        {
            get
            {
                for (var i = 0; i < _m; i++)
                    yield return new Vector<T>(_elements[i]);
            }
        }

        /// <summary>Returns the column vectors.</summary>
        public IEnumerable<Vector<T>> Columns
        {
            get
            {
                for (var j = 0; j < _n; j++)
                {
                    var column = new T[_m];
                    for (var i = 0; i < _m; i++)
                        column[i] = _elements[i][j];

                    yield return new Vector<T>(column);
                }
            }
        }

        public Vector<T> DiagonalVector
        {
            get
            {
                if (!IsSquare)
                    throw new InvalidOperationException("Matrix should be square.");

                var diagonal = new T[_m];
                for (var i = 0; i < _m; i++)
                    diagonal[i] = _elements[i][i];

                return new Vector<T>(diagonal);
            }
        }

        #endregion

        #region Constructors

        public Matrix(T[][] elements)
        {
            _m = elements.Length;
            _n = elements[0].Length;
            _elements = elements;
        }

        public Matrix(int m, int n)
        {
            _m = m;
            _n = n;
            _elements = new T[m][];
            for (var i = 0; i < m; i++)
                _elements[i] = new T[n];
        }

        public Matrix(Vector<T> vector)
        {
            if (vector.IsColumnVector)
            {
                _m = vector.Length;
                _n = 1;
                _elements = new T[_m][];
                for (var i = 0; i < _m; i++)
                    _elements[i] = new[] { vector[i] };
            }
            else
            {
                _m = 1;
                _n = vector.Length;
                _elements = new[] {vector.Elements};
            }
        }

        #endregion

        #region Operators implementation

        public static bool operator ==(Matrix<T> u, Matrix<T> v)
        {
            return u != null && u.Equals(v);
        }

        public static bool operator !=(Matrix<T> u, Matrix<T> v)
        {
            return u != null && !u.Equals(v);
        }

        #endregion
        
        #region ICloneable implementation

        public Matrix<T> Clone()
        {
            return new Matrix<T>(_elements.Select(x => x.Select(y => y).ToArray()).ToArray());
        }

        object ICloneable.Clone()
        {
            return Clone();
        }

        #endregion

        #region IEnumerable, IEnumerable<Vector<T>> implementation

        public IEnumerator<Vector<T>> GetEnumerator()
        {
            for (var i = 0; i < _m; i++)
                yield return new Vector<T>(_elements[i]);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _elements.GetEnumerator();
        }

        #endregion

        #region IEquatable<Matrix<T>> interface

        public bool Equals(Matrix<T> other)
        {
            if (_m != other._m || _n != other._n)
                return false;

            for (var i = 0; i < _m; i++)
                for (var j = 0; j < _n; j++)
                    if (!_elements[i][j].Equals(other._elements[i][j]))
                        return false;

            return true;
        }

        #endregion

        #region Public methods
        
        public Matrix<T> Transpose()
        {
            var transpose = new T[_n][];

            for (var j = 0; j < _n; j++)
            {
                transpose[j] = new T[_m];
                for (var i = 0; i < _m; i++)
                    transpose[j][i] = _elements[i][j];
            }

            return new Matrix<T>(transpose);
        }

        public Matrix<T> SubMatrix(int m1, int m2, int n1, int n2)
        {
            if (m2 < m1 || n2 < n1)
                throw new ArgumentException("Start indices should be smaller than end indices.");

            if (m1 < 0 || m2 > _m || n1 < 0 || n2 > _n)
                throw new ArgumentException("Row and column indices should be within [0, m] and [0, n]");

            var subM = m2 - m1 + 1;
            var subN = n2 - n1 + 1;
            var subMatrix = new T[subM][];

            for (var i = 0; i < subM; i++)
            {
                subMatrix[i] = new T[subN];
                for (var j = 0; j < subN; j++)
                    subMatrix[i][j] = _elements[i + m1][j + n1];
            }

            return new Matrix<T>(subMatrix);
        }

        public Matrix<T> SubMatrix(int[] rowIndices, int[] columnIndices)
        {
            var subM = rowIndices.Length;
            var subN = columnIndices.Length;

            for (var i = 0; i < subM; i++)
                if (rowIndices[i] < 0 || rowIndices[i] > _m)
                    throw new ArgumentException("Row indices should be within [0, m]");

            for (var j = 0; j < subN; j++)
                if (columnIndices[j] < 0 || columnIndices[j] > _n)
                    throw new ArgumentException("Column indices should be within [0, n]");

            var subMatrix = new T[subM][];
            for (var i = 0; i < subM; i++)
            {
                subMatrix[i] = new T[subN];
                for (var j = 0; j < subN; j++)
                    subMatrix[i][j] = _elements[rowIndices[i]][columnIndices[j]];
            }

            return new Matrix<T>(subMatrix);
        }

        public Matrix<T> SubMatrix(int[] rowIndices, int n1, int n2)
        {
            var subN = n2 - n1 + 1;
            var columnIndices = new int[subN];
            for (var j = 0; j < subN; j++)
                columnIndices[j] = n1 + j;

            return SubMatrix(rowIndices, columnIndices);
        }

        public Matrix<T> SubMatrix(int m1, int m2, int[] columnIndices)
        {
            var subM = m2 - m1 + 1;
            var rowIndices = new int[subM];
            for (var i = 0; i < subM; i++)
                rowIndices[i] = m1 + i;

            return SubMatrix(rowIndices, columnIndices);
        }

        #endregion

        #region Object overriden

        public override int GetHashCode()
        {
            return Rows.Aggregate(1, (hashCode, current) => hashCode ^ current.GetHashCode());
        }

        public override bool Equals(object other)
        {
            if (other == null)
                throw new NullReferenceException();

            if (!(other is Matrix<T>))
                throw new InvalidCastException("The 'other' argument is not a Matrix<T> object.");

            return Equals(other as Matrix<T>);
        }

        public override string ToString()
        {
            return String.Format("Matrix<{0}>[{1}x{2}]\r\n", typeof(T), _m, _n);
        }

        #endregion
    }
}
